
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "cargs.h"

#define ENABLE_VALIDATION
#define NULLCHAR 0
#define ALLOCATE(type, amount) \
    (type*)malloc(sizeof(type) * amount)

#define REALLOCATE(pointer, type, amount) \
    (type*)realloc(pointer, sizeof(type) * amount)

#define ADD_ELEM(type, pointer, elem, currLen)\
    if (pointer == NULL) {\
        pointer = ALLOCATE(type, 1);\
        pointer[0] = elem;\
    } else {\
        pointer = REALLOCATE(pointer, type, currLen + 1);\
        pointer[currLen] = elem;\
    }\

#define PTR_NULLSAFE_SET(pointer, value) if (pointer != NULL) *pointer = value

#define OUT

const __cargs_bool __cargs_true = 1; 
const __cargs_bool __cargs_false = 0; 

void parseCurrentArg(
    ParseContext* context, 
    ParseResult* result, 
    const int argc, 
    char* argv[], 
    const size_t pos, 
    OUT size_t* nextPos); 

const __cargs_bool isInteger(char* str)
{
    const size_t len = strlen(str);
    for (size_t i = 0; i < len; i++) {
        if (!isdigit(str[i])) {
            return __cargs_false; 
        }
    } 

    return __cargs_true; 
}

const __cargs_bool isDecimal(char* str) 
{
    const size_t len = strlen(str); 
    for (size_t i = 0; i < len; i++) {
        if (!(isdigit(str[i]) || str[i] == '.')) {
            return __cargs_false;
        }
    }

    return __cargs_true; 
}

const OptionData booleanArg(
    const char shortOption, 
    char* longOption, 
    OUT __cargs_bool* source, 
    char* description) 
{
    OptionData ret = {
        .shortOption = shortOption,
        .type = Boolean,
        .longOption = longOption,
        .description = description,
        .sourcePtr = (void*)source,
    }; 

    PTR_NULLSAFE_SET(source, 0); 
    return ret; 
}

const OptionData integerArg(
    const char shortOption, 
    char* longOption, 
    OUT __cargs_int* source, 
    char* description) 
{
    OptionData ret = {
        .shortOption = shortOption,
        .type = Integer,
        .longOption = longOption,
        .description = description,
        .sourcePtr = (void*)source,
    }; 

    PTR_NULLSAFE_SET(source, 0); 
    return ret; 
}

const OptionData decimalArg(
    const char shortOption, 
    char* longOption, 
    OUT __cargs_decimal* source, 
    char* description) 
{
    OptionData ret = {
        .shortOption = shortOption,
        .type = Decimal,
        .longOption = longOption,
        .description = description,
        .sourcePtr = (void*)source,
    }; 

    PTR_NULLSAFE_SET(source, 0); 
    return ret; 
}

const OptionData stringArg(
    const char shortOption, 
    char* longOption, 
    char** source, 
    char* description) 
{
    OptionData ret = {
        .shortOption = shortOption,
        .type = String,
        .longOption = longOption,
        .description = description,
        .sourcePtr = (void*)source,
    }; 

    PTR_NULLSAFE_SET(source, ""); 
    return ret; 
}

void freeResult(ParseResult* result) 
{
    if (result == NULL) {
        return; 
    }

    free(result->decimalValues);
    free(result->decimalOptions); 
    free(result->errors);
    free(result->freeValues);
    free(result->integerOptions);
    free(result->integerValues);
    free(result->booleanOptions); 
    free(result->stringOptions);
    free(result->stringValues);
    free(result); 
}

ParseResult* parseArgs(ParseContext* context, const int argc, char* argv[]) 
{
    ParseResult* result = ALLOCATE(ParseResult, 1); 
    result->booleanOptionsCount = 0;
    result->decimalOptionsCount = 0;
    result->integerOptionsCount = 0;
    result->stringOptionsCount = 0;
    result->errorsCount = 0; 

    size_t pos = 1; 

    while (pos < argc) {
        size_t nextPos = 0; 
        parseCurrentArg(context, result, argc, argv, pos, &nextPos); 

        pos = nextPos; 
    }

    return result; 
}

typedef enum  
{
    Long, 
    Short, 
    None, 
}
OptionLengthCategory; 

const size_t searchOptionShort(
    ParseContext* context, 
    char shortName, 
    OUT __cargs_bool* success) 
{
    const OptionData* items = context->items; 
    for (size_t i = 0; i < context->itemsCount; i++) {
        if (items[i].shortOption == shortName) {
            *success = __cargs_true; 
            return i; 
        }
    }

    *success = __cargs_false;
    return 0; 
}

const size_t searchOptionLong(
    ParseContext* context, 
    const char* longName, 
    OUT __cargs_bool* success) 
{
    const OptionData* items = context->items; 
    for (size_t i = 0; i < context->itemsCount; i++) {
        if (items[i].longOption == NULL) {
            continue; 
        }

        if (strcmp(items[i].longOption, longName) == 0) {
            *success = __cargs_true; 
            return i; 
        }
    }

    *success = __cargs_false; 
    return 0; 
} 

const OptionLengthCategory getOptionCategory(char* option) 
{
    if (option == NULL) {
        return None; 
    }

    const size_t len = strlen(option); 
    if (strncmp(option, "--", 2) == 0) { // if arg starts with '--'
        return len > 2 ? Long : None; 
    }

    if (strncmp(option, "-", 1) == 0) { // if arg starts with '-'
        return len > 1 ? Short : None; 
    }

    return None; 
}

void processNonBooleanArgs(
    ParseContext* context, 
    ParseResult* result, 
    const size_t optionIndex, 
    const OptionData* data, 
    int argc, 
    char* argv[], 
    const int pos,
    OUT size_t* nextPos) 
{
    if (pos == argc - 1) {
        ParseError error = {
            .category = NonBooleanOptionDoesntHaveArgument,
            .argIndex = pos,
            .optionShortName = NULLCHAR,
        };

        ADD_ELEM(
            ParseError, 
            result->errors, 
            error,
            result->errorsCount); 

        result->errorsCount++; 
        *nextPos = pos + 1;
        return; 
    }

    switch (data->type) {
        case Integer: 
            if (!isInteger(argv[pos + 1])) {
                ParseError error = {
                    .category = IntegerParsingError,
                    .argIndex = pos,
                    .optionShortName = NULLCHAR,
                };

                ADD_ELEM(
                    ParseError, 
                    result->errors, 
                    error, 
                    result->errorsCount); 
                
                result->errorsCount++; 
                *nextPos = pos + 2;
                return; 
            }

            __cargs_int valInt = (__cargs_int)atoi(argv[pos + 1]); 
            ADD_ELEM(
                __cargs_int, 
                result->integerValues, 
                valInt, 
                result->integerOptionsCount);
             
            ADD_ELEM(
                size_t, 
                result->integerOptions, 
                optionIndex, 
                result->integerOptionsCount);
             
            result->integerOptionsCount++; 
            *nextPos = pos + 2;

            __cargs_int* sourceInt = (__cargs_int*)data->sourcePtr;
            PTR_NULLSAFE_SET(sourceInt, valInt); 
            
            break; 
        case String: 
            { }
            char* valStr = argv[pos + 1]; 
            ADD_ELEM(
                char*, 
                result->stringValues, 
                valStr, 
                result->stringOptionsCount);
             
            ADD_ELEM(
                size_t, 
                result->stringOptions, 
                optionIndex, 
                result->stringOptionsCount);
             
            result->stringOptionsCount++; 
            *nextPos = pos + 2;

            char** sourceStr = (char**)data->sourcePtr; 
            PTR_NULLSAFE_SET(sourceStr, valStr); 

            break; 
        case Decimal:
            if (!isDecimal(argv[pos + 1])) {
                ParseError error = {
                    .category = DecimalParsingError,
                    .argIndex = pos,
                    .optionShortName = NULLCHAR,
                };

                ADD_ELEM(
                    ParseError, 
                    result->errors, 
                    error, 
                    result->errorsCount); 
                
                result->errorsCount++; 
                *nextPos = pos + 2;
                return; 
            }

            __cargs_decimal valDecimal = (__cargs_decimal)atof(argv[pos + 1]); 
            ADD_ELEM(
                __cargs_decimal, 
                result->decimalValues, 
                valDecimal, 
                result->decimalOptionsCount); 
            
            ADD_ELEM(
                size_t, 
                result->decimalOptions, 
                valDecimal, 
                result->decimalOptionsCount); 
            
            result->decimalOptionsCount++; 
            *nextPos = pos + 2;

            __cargs_decimal* sourceDbl = (__cargs_decimal*)data->sourcePtr; 
            PTR_NULLSAFE_SET(sourceDbl, valDecimal); 
            
            break; 
        default: 
            *nextPos = pos + 1; 
            break; 
    }   
}

void parseCurrentArg(
    ParseContext* context, 
    ParseResult* result, 
    const int argc, 
    char* argv[], 
    const size_t pos, 
    OUT size_t* nextPos) 
{
    char* arg = argv[pos]; 
    const OptionLengthCategory cat = getOptionCategory(arg); 
    *nextPos = pos; 

    
    OptionData* items = context->items;   
    switch (cat) {
        case Long: 
            goto longProcessing; 
            
            return;
        case Short:     
            goto shortProcessing; 

            break; 
        case None: 
            ADD_ELEM(char*, result->freeValues, arg, result->freeValuesCount); 
            result->freeValuesCount++; 
            *nextPos = *nextPos + 1;
            break; 
    }

    return; 

    // TODO: REFACTOR THIS!!!
longProcessing: 
    // arg + 2 ->  pointer to start of a arg name (after --)
    { }
    __cargs_bool searchSuccess = __cargs_false; 
    size_t optionIndex = searchOptionLong(context, arg + 2, &searchSuccess);
    if (!searchSuccess) {
        ParseError error = {
            .category = OptionDoesntExist,
            .argIndex = pos,
            .optionShortName = NULLCHAR,
        };

        ADD_ELEM(ParseError, result->errors, error, result->errorsCount); 
        result->errorsCount++; 
        *nextPos = *nextPos + 1;
        return; 
    }

    OptionData option = items[optionIndex];
    if (option.type != Boolean) {
        processNonBooleanArgs(
            context, result, optionIndex, &option, argc, argv, pos, nextPos); 

        return; 
    }

    ADD_ELEM(
        size_t, 
        result->booleanOptions, 
        optionIndex, 
        result->booleanOptionsCount); 

    __cargs_bool* srcBool = (__cargs_bool*)option.sourcePtr;
    PTR_NULLSAFE_SET(srcBool, __cargs_true); 
    *nextPos = *nextPos + 1;

    result->booleanOptionsCount++; 
    return; 

    // TODO: REFACTOR THIS!!!
shortProcessing:
    { }
    const size_t len = strlen(arg);
    if (len == 2) {
        __cargs_bool searchSuccess = __cargs_false; 
        size_t optionIndex = searchOptionShort(context, arg[1], &searchSuccess);
        if (!searchSuccess) {
            ParseError error = {
                .category = OptionDoesntExist,
                .argIndex = pos,
                .optionShortName = NULLCHAR,
            };

            ADD_ELEM(ParseError, result->errors, error, result->errorsCount); 
            result->errorsCount++; 
            *nextPos = *nextPos + 1;
            return; 
        }

        OptionData option = items[optionIndex];
        if (option.type != Boolean) {
            processNonBooleanArgs(
                context, result, optionIndex, 
                &option, argc, argv, pos, nextPos); 

            return; 
        }

        ADD_ELEM(
            size_t, 
            result->booleanOptions, 
            optionIndex, 
            result->booleanOptionsCount);
         
        __cargs_bool* srcBool = (__cargs_bool*)option.sourcePtr;
        PTR_NULLSAFE_SET(srcBool, __cargs_true); 
        *nextPos = *nextPos + 1;        
    }
    else {
        // args should be boolean
        for (size_t i = 1; i < len; i++) {
            __cargs_bool charSuccess = __cargs_false; 
            const size_t index = searchOptionShort(context, arg[i], &charSuccess);
            if (!charSuccess) {
                ParseError error = {
                    .category = OptionDoesntExist,
                    .argIndex = pos,
                    .optionShortName = arg[i],
                };

                ADD_ELEM(
                    ParseError, 
                    result->errors, 
                    error, 
                    result->errorsCount); 
                
                result->errorsCount++; 
                continue; 
            }

            OptionData option = items[index]; 
            if (option.type != Boolean) {
                ParseError error = {
                    .category = CombinedOptionsShouldBeBoolean,
                    .argIndex = pos,
                    .optionShortName = option.shortOption,
                };

                ADD_ELEM(
                    ParseError, 
                    result->errors, 
                    error, 
                    result->errorsCount); 
                
                result->errorsCount++;  

                continue; 
            }   

            ADD_ELEM(
                size_t, 
                result->booleanOptions, 
                optionIndex, 
                result->booleanOptionsCount); 
            
            __cargs_bool* srcBool = (__cargs_bool*)option.sourcePtr;
            PTR_NULLSAFE_SET(srcBool, __cargs_true);  
        }

        *nextPos = *nextPos + 1; 
    }
}

const __cargs_bool fprintErrors(
    FILE* file, 
    ParseContext* context, 
    ParseResult* result, 
    const int argc, 
    char* argv[])
{
    ParseError* errors = result->errors; 
    __cargs_bool status = __cargs_false; 
    
    for (size_t i = 0; i < result->errorsCount; i++) {
        const ParseError error = errors[i]; 
        const ParseErrorCategory cat = error.category; 
        
        char* stringFormat; 
        switch (cat) {
            case OptionDoesntExist:
                stringFormat = "%s: Unrecognized option '%s'\n"; 
                break; 
            case NonBooleanOptionDoesntHaveArgument:
                stringFormat = "%s: Option '%s' requires an argument\n"; 
                break; 
            case CombinedOptionsShouldBeBoolean: 
                stringFormat = "%s: Option '%s' is not boolean so cannot be combined\n"; 
                break; 
            case IntegerParsingError:
                stringFormat = "%s: Unable to cast '%s' argument value to integer\n"; 
                break; 
            case DecimalParsingError: 
                stringFormat = "%s: Unable to cast '%s' argument value to decimal\n"; 
                break; 
        }

        if (error.optionShortName == NULLCHAR) {
            fprintf(file, stringFormat, argv[0], argv[error.argIndex]);
        }
        else {
            char argName[4]; 
            sprintf(OUT argName, "-%c", error.optionShortName); 
            fprintf(file, stringFormat, argv[0], argName); 
        }
        
        status = __cargs_true; 
    }

    return status; 
}

const __cargs_bool printErrors(
    ParseContext* context, 
    ParseResult* result, 
    const int argc, 
    char* argv[]) 
{
    return fprintErrors(stderr, context, result, argc, argv); 
}

void fprintHelp(FILE* file, ParseContext* context) 
{
    OptionData* options = context->items; 

    size_t maxLongNameLen = 0; 
    for (size_t i = 0; i < context->itemsCount; i++) {
        const OptionData option = options[i]; 
        if (option.longOption == NULL) {
            continue;
        }

        const size_t longNameLen = strlen(option.longOption); 

        if (longNameLen > maxLongNameLen) {
            maxLongNameLen = longNameLen; 
        }
    }

    maxLongNameLen += 2; // --optionName

    char format[64]; 
    sprintf(format, "  %%s%%c %%%zus    %%s\n", maxLongNameLen); // %zu - print size_t
    size_t nextLinesWhitespaceLength =  
        + 2 // 2 spaces
        + 2 // smallName (-s)
        + 1 // delimiter
        + 1 // 1 space
        + maxLongNameLen // longName (--long)
        + 4; // 4 spaces
    
    char nextLineFormat[64]; 
    sprintf(nextLineFormat, "%%%zus%%s\n", nextLinesWhitespaceLength + 2); 

    char* longName = ALLOCATE(char, maxLongNameLen + 2); 
    for (size_t i = 0; i < context->itemsCount; i++) {
        const OptionData option = options[i]; 
        char delimiter = ' '; 
        char shortName[4];

        if (option.shortOption != NULLCHAR && option.longOption != NULL) {
            delimiter = ','; 
        }

        if (option.shortOption == NULLCHAR) {
            sprintf(shortName, "  "); 
        }
        else {
            sprintf(shortName, "-%c", option.shortOption); 
        }
        
        if (option.longOption == NULL) {
            sprintf(longName, " "); 
        }
        else {
            sprintf(longName, "--%s", option.longOption); 
        }

        char* description; 
        if (option.description == NULL) {
            description = " "; 
        } 
        else {
            description = option.description; 
        }

        size_t descriptionLength = strlen(description); 

        // required for strtok_r 
        char* dynDescription = ALLOCATE(char, descriptionLength + 2);
        strcpy(dynDescription, description);

        char* tokSavePtr; 
        char* delim = "\n"; 
        char* token = strtok_r(dynDescription, delim, &tokSavePtr); 
        if (token == NULL) {
            token = ""; 
        }

        fprintf(file, format, shortName, delimiter, longName, token);

        while (__cargs_true) {
            token = strtok_r(NULL, delim, &tokSavePtr); 
            if (token == NULL) {
                break; 
            }

            fprintf(file, nextLineFormat, " ", token); 
        }

        free(dynDescription);         
    }
    free(longName); 
}

void printHelp(ParseContext* context) 
{
    fprintHelp(stdout, context); 
}